import os.path
import logging

from validr import T, Invalid, fields, mark_key

from tinyops.library.validator import compiler
from tinyops.library.task import TaskParser, TargetType


LOG = logging.getLogger(__name__)


def _process_default(config):
    DEFAULT_NOTIFIER_SCHEMA = T.dict(
        smtp=T.dict(
            host=T.str.optional,
            port=T.int.min(0).default(25),
            use_ssl=T.bool.default(False),
            username=T.str.optional,
            password=T.str.optional,
            timeout=T.timedelta.default(30),
        ),
        server_chan=T.dict(
            endpoint=T.str.default("https://sctapi.ftqq.com"),
            timeout=T.timedelta.default(30),
        )
    )
    DEFAULT_SCHEMA = T.dict(
        server=T.dict(
            port=T.int.min(0).default(22),
            user=T.str.default('root'),
            password=T.str.optional,
            public_key=T.path.optional.desc('Path to SSH public key'),
            private_key=T.path.optional.desc('Path to SSH private key'),
            shell=T.str.optional,
            timeout=T.timedelta.default(1800),
        ),
        docker=T.dict(
            tls=T.bool.optional.desc('Use TLS; implied by tlsverify'),
            tlscacert=T.path.optional.desc('Trust certs signed only by this CA'),
            tlscert=T.path.optional.desc('Path to TLS certificate file'),
            tlskey=T.path.optional.desc('Path to TLS key file'),
            tlsverify=T.bool.optional.desc('Use TLS and verify the remote'),
            timeout=T.timedelta.default(1800),
        ),
        task=T.dict(
            timeout=T.timedelta.default(3600),
        ),
        webhook=T.dict(
            secret=T.str.optional,
        ),
        notifier=DEFAULT_NOTIFIER_SCHEMA,
    )
    validate_default = compiler.compile(DEFAULT_SCHEMA)
    default_config = config.get('default') or {}
    for k in fields(DEFAULT_SCHEMA):
        default_config.setdefault(k, {})
    for k in fields(DEFAULT_NOTIFIER_SCHEMA):
        default_config['notifier'].setdefault(k, {})
    default_config = validate_default(default_config)
    return default_config


def process_config(config):
    DEFAULT = default_config = _process_default(config)
    WEBUI_SCHEMA = T.dict(
        host=T.str.default('0.0.0.0'),
        port=T.int.min(0).default(9010),
        webhook_port=T.int.min(0).default(9011),
        subpath=T.str.default('/tinyops'),
        webhook_url_prefix=T.str.optional,
        basic_auth=T.dict(
            username=T.str.default('admin'),
            password=T.str.default('admin'),
        ).optional,
        threads=T.int.min(5).default(100),
        access_logfile=T.str.default('-'),
        error_logfile=T.str.default('-'),
    )
    SERVER_SCHEMA = T.dict(
        name=T.str,
        label=T.str_list.optional,
        host=T.str,
        port=T.int.min(0).default(DEFAULT['server']['port']),
        user=T.str.default(DEFAULT['server']['user']),
        password=T.str.optional.default(DEFAULT['server']['password']),
        private_key=T.path.optional.default(DEFAULT['server']['private_key']),
        timeout=T.timedelta.default(DEFAULT['server']['timeout']),
    )
    DOCKER_SCHEMA = T.dict(
        name=T.str,
        label=T.str_list.optional,
        host=T.str.desc('eg: unix://var/run/docker.sock tcp://0.0.0.0:2375'),
        tls=T.bool.optional.default(DEFAULT['docker']['tls']),
        tlscacert=T.path.optional.default(DEFAULT['docker']['tlscacert']),
        tlscert=T.path.optional.default(DEFAULT['docker']['tlscert']),
        tlskey=T.path.optional.default(DEFAULT['docker']['tlskey']),
        tlsverify=T.bool.optional.default(DEFAULT['docker']['tlsverify']),
        timeout=T.timedelta.default(DEFAULT['server']['timeout']),
    )
    TASK_SCHEMA = T.dict(
        name=T.str,
        script=T.str.optional,
        script_file=T.path.optional,
        notifier=T.str_list.default('default'),
        timeout=T.timedelta.default(DEFAULT['task']['timeout']),
    )
    TIMER_SCHEMA = T.dict(
        name=T.str.optional,
        task=T.str,
        timer=T.timer,
    )
    WEBHOOK_SCHEMA = T.dict(
        name=T.str.optional,
        task=T.str,
        secret=T.str.default(DEFAULT['webhook']['secret']),
    )
    ALERTER_SCHEMA = T.dict(
        notifier=T.str_list.default('default'),
        secret=T.str,
    )
    NOTIFIER_SCHEMA = T.union(
        group=T.dict(
            name=T.str,
            group=T.str_list.optional,
        ),
        smtp=T.dict(
            name=T.str,
            host=T.str.default(DEFAULT['notifier']['smtp']['host']),
            port=T.int.min(0).default(DEFAULT['notifier']['smtp']['port']),
            use_ssl=T.bool.default(DEFAULT['notifier']['smtp']['use_ssl']),
            username=T.str.default(DEFAULT['notifier']['smtp']['username']),
            password=T.str.default(DEFAULT['notifier']['smtp']['password']),
            receivers=T.list(T.email).minlen(1),
            timeout=T.timedelta.default(DEFAULT['notifier']['smtp']['timeout']),
        ),
        server_chan=T.dict(
            name=T.str,
            endpoint=T.str.default(DEFAULT['notifier']['server_chan']['endpoint']),
            sckey=T.str,
            timeout=T.timedelta.default(DEFAULT['notifier']['server_chan']['timeout']),
        )
    ).by('type')
    CONFIG_SCHEMA = T.dict(
        debug=T.bool.default(False),
        filepath=T.path.optional,
        timezone=T.int.default(0),
        timezone_name=T.str.default('UTC'),
        directory=T.dict(
            logs=T.path.optional,
            data=T.path.optional,
            temp=T.path.optional,
        ),
        webui=WEBUI_SCHEMA,
        alerter=ALERTER_SCHEMA.optional,
        notifiers=T.list(NOTIFIER_SCHEMA),
        servers=T.list(SERVER_SCHEMA),
        dockers=T.list(DOCKER_SCHEMA),
        tasks=T.list(TASK_SCHEMA),
        timers=T.list(TIMER_SCHEMA),
        webhooks=T.list(WEBHOOK_SCHEMA),
    )
    validate_config = compiler.compile(CONFIG_SCHEMA)
    config.setdefault('directory', {})
    config.setdefault('webui', {})
    config.setdefault('notifiers', [])
    config.setdefault('servers', [])
    config.setdefault('dockers', [])
    config.setdefault('tasks', [])
    config.setdefault('timers', [])
    config.setdefault('webhooks', [])
    config = validate_config(config)
    config['default'] = default_config
    config['notifiers'] = _process_notifiers(config)
    config['alerter'] = _process_alerter(config)
    config['tasks'] = _process_tasks(config)
    _resolve_task_notifier_names(config)
    config['directory'] = _process_directory(config)
    config = _post_validate_config(config)
    return config


def _process_notifiers(config: dict) -> list:
    notifiers = {}
    for x in config['notifiers']:
        notifiers[x['name']] = x
    notifiers.setdefault('default', {
        'type': 'group',
        'name': 'default',
        'group': [],
    })
    return list(notifiers.values())


def _process_alerter(config: dict) -> dict:
    if config['alerter']:
        names = config['alerter']['notifier']
        notifier = resolve_notifiers(config, {'x': names})['x']
        return dict(config['alerter'], notifier=notifier)
    return None


def _process_tasks(config: dict) -> list:
    TASK_STEP_SCHEMA = T.dict(
        lineno=T.int,
        source=T.str,
        command=T.str,
        targets=T.list(T.union({
            TargetType.LOCAL: T.dict(
                name=T.str,
                image=T.str.optional,
            ),
            TargetType.SERVER: T.dict(
                name=T.str,
                image=T.str.optional,
            ),
            TargetType.DOCKER: T.dict(
                name=T.str,
                image=T.str,
            )
        }).by('type')),
        params=T.dict,
    )
    validate_task_steps = compiler.compile(T.list(TASK_STEP_SCHEMA))
    tasks = []
    with mark_key('tasks'):
        for task in config['tasks']:
            with mark_key('[name="{}"]'.format(task['name'])):
                steps = TaskParser.parse(
                    script=task['script'], script_file=task['script_file'])
                with mark_key('steps'):
                    task['steps'] = validate_task_steps(steps)
                tasks.append(task)
    return tasks


def _process_directory(config: dict):
    filepath = config['filepath']
    dir_config = config['directory']
    if not filepath:
        return dir_config
    conf_dir = os.path.dirname(filepath)
    for name in ['temp', 'logs', 'data']:
        x_dir = os.path.join(os.path.dirname(conf_dir), name)
        if not dir_config[name]:
            dir_config[name] = x_dir
    return dir_config


def resolve_notifiers(config: dict, notifiers: dict):
    notifiers_map = {}
    for x in config['notifiers']:
        notifiers_map[x['name']] = x
    ret = {}
    for key, names in notifiers.items():
        resolved_names = set()
        visited_names = set()
        temp_names = set(names)
        while temp_names:
            name = temp_names.pop()
            if name in visited_names:
                continue
            visited_names.add(name)
            notifier_config = notifiers_map.get(name)
            if notifier_config is None:
                raise Invalid(f'notifier {name} not exists')
            if notifier_config['type'] == 'group':
                temp_names.update(notifier_config['group'] or [])
            else:
                resolved_names.add(name)
        ret[key] = list(resolved_names)
    return ret


def _resolve_task_notifier_names(config: dict):
    notifiers = {}
    for task in config['tasks']:
        notifiers[task['name']] = task['notifier']
    resolved_notifiers = resolve_notifiers(config, notifiers)
    for task in config['tasks']:
        task['notifier'] = resolved_notifiers[task['name']]


def _post_validate_config(config: dict):
    # validate targets
    target_names = set()
    server_targets = {}
    docker_targets = {}
    for x in config['servers']:
        if x['name'] in target_names:
            raise Invalid(f"server name {x['name']} conflict")
        target_names.add(x['name'])
        server_targets[x['name']] = x
    for x in config['dockers']:
        if x['name'] in target_names:
            raise Invalid(f"docker name {x['name']} conflict")
        target_names.add(x['name'])
        docker_targets[x['name']] = x

    # validate tasks
    tasks = {}
    for x in config['tasks']:
        if x['name'] in tasks:
            raise Invalid(f"task name {x['name']} conflict")
        tasks[x['name']] = x
        for i, step in enumerate(x['steps']):
            step_name = f"task {x['name']} step#{i}"
            for target in step['targets']:
                target_info = f"{step_name} target {target['name']}"
                if target['type'] == TargetType.DOCKER:
                    if target['name'] not in docker_targets:
                        raise Invalid(f'{target_info} not exists')
                    if not target['image']:
                        raise Invalid(f'{target_info} image is required')
                if target['type'] == TargetType.SERVER:
                    if target['name'] not in server_targets:
                        raise Invalid(f'{target_info} not exists')
                    target['image'] = None

    for x in config['webhooks']:
        if not x['name']:
            x['name'] = x['task']
        if x['task'] not in tasks:
            raise Invalid(f"webhook {x['name']} task {x['task']} not exists")

    for x in config['timers']:
        if not x['name']:
            x['name'] = x['task']
        if x['task'] not in tasks:
            raise Invalid(f"timer {x['name']} task {x['task']} not exists")

    return config
